﻿using System;
using System.ComponentModel;
using System.Linq;
using System.Xml;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;
using Notoric.Model.Validation;

namespace Notoric.Model.Elements.CWNotation
{
    /// <summary>
    /// Represents a barline.
    /// </summary>
    public class Barline : ModelElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="Barline"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="System"/>
        /// </para>
        /// <para>
        /// The system that directly owns this barline.
        /// </para>
        /// </param>
        /// <param name="left">
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// </param>
        /// <param name="right">
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="owner"/> is <see langword="null"/>.
        /// </exception>
        internal Barline(XmlPackagePart packagePart, System owner, MeasureElement left, MeasureElement right)
            : base(packagePart, owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            this.left = left;
            this.right = right;

            if (left != null)
            {
                leftAttribute.Value = left.Number.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                left.PropertyChanged += new PropertyChangedEventHandler(left_PropertyChanged);
            }
            else
                leftAttribute.Value = "0";

            if (right != null)
            {
                rightAttribute.Value = right.Number.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                right.PropertyChanged += new PropertyChangedEventHandler(right_PropertyChanged);
            }
            else
                rightAttribute.Value = "0";

            if (left != null)
                left.FindOwner<CompositionElement>().PropertyChanged += new PropertyChangedEventHandler(Composition_PropertyChanged);
            else if (right != null)
                right.FindOwner<CompositionElement>().PropertyChanged += new PropertyChangedEventHandler(Composition_PropertyChanged);
            
            positionAttribute.Value = (0.0).ToString(PackageVocabulary.PackageCulture.NumberFormat);
            typeAttribute.Value = BarlineType.Single.ToString();
            SetBarlineType();
        }

        /// <summary>
        /// Creates a new <see cref="Barline"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="System"/>
        /// </para>
        /// <para>
        /// The system that directly owns this barline.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal Barline(XmlPackagePart packagePart, System owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            leftAttribute = element.Attributes[PackageVocabulary.LeftAttribute];
            rightAttribute = element.Attributes[PackageVocabulary.RightAttribute];
            typeAttribute = element.Attributes[PackageVocabulary.BarlineTypeAttribute];
            positionAttribute = element.Attributes[PackageVocabulary.PositionAttribute];

            var composition = FindOwner<MusicSectionElement>().Composition.Composition;
            var ln = ushort.Parse(leftAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat);
            var rn = ushort.Parse(rightAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat);

            if (ln >= 1 && ln <= composition.MeasuresCount)
            {
                left = composition[ln];
                left.PropertyChanged += new PropertyChangedEventHandler(left_PropertyChanged);
            }
            else if (ln == 0)
                left = null;
            else
                throw new SerializationException("The measure number " + ln + " is not in the presented composition!", element.OuterXml);

            if (rn >= 1 && rn <= composition.MeasuresCount)
            {
                right = composition[rn];
                right.PropertyChanged += new PropertyChangedEventHandler(right_PropertyChanged);
            }
            else if (rn == 0)
                right = null;
            else
                throw new SerializationException("The measure number " + rn + " is not in the presented composition!", element.OuterXml);

            if (left != null)
                left.FindOwner<CompositionElement>().PropertyChanged += new PropertyChangedEventHandler(Composition_PropertyChanged);
            else if (right != null)
                right.FindOwner<CompositionElement>().PropertyChanged += new PropertyChangedEventHandler(Composition_PropertyChanged);
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.BarlineElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            leftAttribute = document.CreateAttribute(PackageVocabulary.LeftAttribute);
            positionAttribute = document.CreateAttribute(PackageVocabulary.PositionAttribute);
            rightAttribute = document.CreateAttribute(PackageVocabulary.RightAttribute);
            typeAttribute = document.CreateAttribute(PackageVocabulary.BarlineTypeAttribute);

            element.Attributes.Append(leftAttribute);
            element.Attributes.Append(positionAttribute);
            element.Attributes.Append(rightAttribute);
            element.Attributes.Append(typeAttribute);

            return element;
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Handles the PropertyChanged event of the Composition that owns the measures..
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">Event data.</param>
        private void Composition_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "MeasuresCount")
                SetBarlineType();
        }

        /// <summary>
        /// Occurs when the value of some property of the <see cref="left"/> measure has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// The handler modifies the type of the barline.
        /// </remarks>
        /// <param name="sender">Object that has raised the event</param>
        /// <param name="e">Event data</param>
        private void left_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            SetBarlineType();
        }

        /// <summary>
        /// Occurs when the value of some property of the <see cref="right"/> measure has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// The handler modifies the type of the barline.
        /// </remarks>
        /// <param name="sender">Object that has raised the event</param>
        /// <param name="e">Event data</param>
        private void right_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            SetBarlineType();
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Sets the type of the barline according to the types of its left and right measures.
        /// </summary>
        private void SetBarlineType()
        {
            BarlineType leftType, rightType;

            if (left != null)
            {
                if (left.Repetition != null)
                {
                    if (left == left.Repetition.LastMeasure)
                        leftType = BarlineType.RepetitionClose;
                    else
                        leftType = BarlineType.Single;
                }
                else
                    leftType = BarlineType.Single;
            }
            else
                leftType = BarlineType.Single;

            if (right != null)
            {
                if (right.Repetition != null)
                {
                    if (right == right.Repetition.FirstMeasure)
                        rightType = BarlineType.RepetitionOpen;
                    else
                        rightType = BarlineType.Single;
                }
                else
                    rightType = BarlineType.Single;
            }
            else if (left != null && (left.Number == left.FindOwner<CompositionElement>().Measures.Count || left.Number == left.FindOwner<CompositionElement>().MeasuresCount))
                rightType = BarlineType.Final;
            else
                rightType = BarlineType.Single;


            BarlineType resultType;
            if (leftType == BarlineType.Single)
                resultType = rightType;
            else if (rightType == BarlineType.Single)
                resultType = leftType;
            else if (rightType == BarlineType.Final)
                resultType = leftType;
            else
                resultType = BarlineType.RepetitionBoth;

            if (resultType != BarlineType.Single || Type != BarlineType.Double)
                Type = resultType;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets a reference to the measure that is on the left side of this barline.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// The measure that is on the left side of this barline. 
        /// A dual definition is possible: This barline is the right barline of the measure.<br/>
        /// If this value is <see langword="null"/> this barline is the first barline in the system (sometimes called systemic barline).
        /// </para>
        /// </value>
        public MeasureElement Left
        {
            get { return left; }
            set
            {
                left = value;
                if (left != null)
                    leftAttribute.Value = left.ID.ToString();
                else
                    leftAttribute.Value = "0";

                SetBarlineType();
                RaisePropertyChanged("Left");
            }
        }

        /// <summary>
        /// Gets or sets the position of this barline.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The offset (in centimeters) of the barline from the beginning of the system.<br/>
        /// The distance between two consecutive barlines defines the width of the measure between them.
        /// </para>
        /// </value>
        /// <exception cref="ValidationException">
        /// The <paramref name="value"/> is less than 0.0.
        /// </exception>
        /// TODO: Write the validation rules
        public double Position
        {
            get { return double.Parse(positionAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                ValidateAndThrow("Position", value);

                positionAttribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                RaisePropertyChanged("Position");
            }
        }

        /// <summary>
        /// Gets or sets a reference to the measure that is on the right side of this barline.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// The measure that is on the right side of this barline.
        /// A dual definition is possible: This barline is the left barline of the measure.<br/>
        /// If this value is <see langword="null"/> this barline is the last barline in the system.
        /// </para>
        /// </value>
        public MeasureElement Right
        {
            get { return right; }
            set
            {
                right = value;
                if (right != null)
                    rightAttribute.Value = right.ID.ToString();
                else
                    rightAttribute.Value = "0";

                SetBarlineType();
                RaisePropertyChanged("Right");
            }
        }

        /// <summary>
        /// Gets or sets the type of the barline.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="BarlineType"/>
        /// </para>
        /// <para>
        /// The type of the barline.
        /// </para>
        /// </value>
        /// TODO: Implement the manipulation of the surrounding measures when the type is repetition.
        public BarlineType Type
        {
            get { return (BarlineType)Enum.Parse(typeof(BarlineType), typeAttribute.Value); }
            set
            {
                typeAttribute.Value = value.ToString();

                RaisePropertyChanged("Type");
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="Left"/> property.
        /// </summary>
        private MeasureElement left;
        /// <summary>
        /// Holds the XML attribute that holds the value of the <see cref="Left"/> property.
        /// </summary>
        private XmlAttribute leftAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Position"/> property.
        /// </summary>
        private XmlAttribute positionAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Right"/> property.
        /// </summary>
        private MeasureElement right;
        /// <summary>
        /// Holds the XML attribute that holds the value of the <see cref="Right"/> property.
        /// </summary>
        private XmlAttribute rightAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Type"/> property.
        /// </summary>
        private XmlAttribute typeAttribute;

        #endregion
    }

    /// <summary>
    /// Describes the type of the barline.
    /// </summary>
    public enum BarlineType
    {
        /// <summary>
        /// Single barline.
        /// </summary>
        Single,
        /// <summary>
        /// Left repetition barline.
        /// </summary>
        RepetitionOpen,
        /// <summary>
        /// Right repetition barline.
        /// </summary>
        RepetitionClose,
        /// <summary>
        /// Repetition end on left side and repetition start on right side.
        /// </summary>
        RepetitionBoth,
        /// <summary>
        /// Double barline.
        /// </summary>
        Double,
        /// <summary>
        /// Final barline.
        /// </summary>
        Final
    }
}
